目录
默认成员函数
构造函数
特性
补丁:
默认构造函数
析构函数
特性
拷贝构造函数
特征
拷贝构造函数典型调用场景:
我们知道一个空类中什么成员都没有,但是事实上它并不是真的什么都没有
因为编译器会自动生成6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
构造函数的主要任务是初始化对象
1. 函数名与类名相同
2. 无返回值
3. 对象实例化时编译器自动调用对应的构造函数
4. 构造函数可以重载
重载实例:
class Date
{
public:
//无参构造函数
Date()
{
}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 7, 23);
d1.Print();
d2.Print();
return 0;
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
错误实例:
Date d3();//错误
5 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成
我们不写构造函数,编译器会生成一个无参的默认构造函数,下面代码可以编译通过
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
若是我们写了构造函数,则编译器不会自动生成无参的默认构造函数,则下面代码编译报错:
因为d1创建后需要调用无参的构造函数,但是没有
class Date
{
public:
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//需要无参的构造函数
d1.Print();
return 0;
}
6 调用编译器自动生成的默认构造函数,可能会对它的作用提出疑惑,怎么调用它之后对象还是随机值呢?
因为默认构造函数对于内置类型的成员不做处理(除非声明时给缺省值)
对自定义类型的成员才做处理,会去调用它的默认构造函数
内置类型就是语言提供的数据类型,如:int/char等
自定义类型就是我们使用class/struct/union等自己定义的类型
实例:
class A
{
public:
A()
{
cout << "A()" << endl;//仅是为了展示调用这个构造函数
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _a;
};
int main()
{
Date d1;
d1.Print();//d1内置类型的变量还是随机值
return 0;
}
总结:一般情况都需要我们自己写构造函数,决定初始化方式
成员变量全是自定义类型,可以考虑不写构造函数
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁
内置类型成员变量在类中声明时可以给默认值(缺省值)
我们不写构造函数时,对于编译器自动生成的默认构造函数进行的改造:内置类型和自定义类型都完成初始化
class A
{
public:
A()
{
cout << "A()" << endl;//仅是为了展示调用这个构造函数
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 2023;//给缺省值
int _month = 7;
int _day = 22;
A _a;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都是默认构造函数
默认构造函数只能有一个,因为多个默认构造函数存在,会出现调用歧义
简而言之,不传参就可以调用的构造就是默认构造
错误实例:
class Date
{
public:
//无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 2023;//给缺省值
int _month = 7;
int _day = 22;
};
int main()
{
Date d1;//调用无参构造函数可以,调用全缺省构造函数也可以,到底调用谁?
d1.Print();
return 0;
}
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,注意:析构
函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
class Stack
{
public:
Stack(int n = 4)
{
cout << "Stack(int n = 4)" << endl;//仅是为了展示调用此构造函数
if (n == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("realloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
~Stack()
{
cout << "~Stack()" << endl;//仅是为了展示调用此析构函数
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
5 系统自动生成的默认析构函数,对内置类型的成员不做处理,对自定义类型的成员会去调用它的析构函数
class Date
{
public:
Date(int year = 1 , int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month ;
int _day ;
};
class Stack
{
public:
Stack(int n = 4)
{
cout << "Stack(int n = 4)" << endl;//仅是为了展示调用此构造函数
if (n == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("realloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
~Stack()
{
cout << "~Stack()" << endl;//仅是为了展示调用此析构函数
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
Date _b;//自定义类型成员
};
int main()
{
Stack st;
return 0;
}
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类,需要在堆上申请空间
7 后定义的对象,先析构
先定义的对象,先构造
class Stack
{
public:
Stack(int n = 4)
{
cout << "Stack(int n = 4)" <<"->"<"<
一个问题:在创建对象时,我们可不可以创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须是同类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用,因此需要传引用传参
正确实例:
class Date
{
public:
Date(int year = 1 , int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month ;
int _day ;
};
int main()
{
Date d1(2023,7,23);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
d2创建后的值和d1一模一样,是因为我们写了拷贝构造函数
若是没有拷贝构造函数,则d2创建后只会是由构造函数初始化后的值
比如:
int main()
{
Date d1(2023,7,23);
Date d2;
d1.Print();
d2.Print();
return 0;
}
d2要作为d1的拷贝,编译器会去自动调用拷贝构造函数,在调用拷贝构造函数之前需要先传参,又是类类型对象之间的传值传参(拷贝),则又要去调用拷贝构造函数,又要先传参……如此往复形成无穷递归
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
编译生成的默认拷贝构造函数:
对内置类型,值拷贝
对自定义的类型,调用他的拷贝构造函数
class A
{
public:
A(int hour = 1, int minute = 1, int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
A(const A& d)
{
cout << "A(const A& d)" << endl;
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1 , int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month ;
int _day ;
A _b;
};
int main()
{
Date d1(2023, 7, 23);
Date d2(d1);
d2.Print();
return 0;
}
Date类我们没有写显示的拷贝构造函数,编译器生成的默认构造函数会对内置类型的成员进行值拷贝,对自定义类型的成员去调用它的拷贝构造函数
Date d2(d1);
Date d2 = d1;
//二者写法等价,都是把d1拷贝给d2
1 使用已存在对象创建新对象
2 函数参数类型为类类型对象
3函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝
下面我们来看一个类:我们自己不写拷贝构造函数,由编译器生成默认构造函数
class Stack
{
public:
Stack(int n = 4)
{
if (n == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
Stack s2(s1);
return 0;
}
程序直接崩溃了,原因如下:
我们没有显示定义一个拷贝构造函数,将栈s1拷贝给栈s2时,编译器生成的默认拷贝构造函数会对内置类型进行值拷贝,那么s2中的_a也指向了s1中的_a指向的堆上的数组空间
当s2的生命周期结束后,编译器会先调用s2的析构函数,对堆上申请的数组空间进行释放
当s1的生命周期结束后,编译器又会调用s1的析构函数,对堆上申请的数组空间进行释放
一块内存空间多次释放,肯定会导致程序崩溃
解决方法:我们自己写拷贝构造函数,深拷贝
让s2中的_a指向另一块空间(一块数据与s1指向的一模一样的空间)
正确实例:
class Stack
{
public:
Stack(int n = 4)
{
if (n == 0)
{
_a = nullptr;
_top = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
}
Stack(const Stack& s)
{
_a = (int*)malloc(sizeof(int) * s._capacity);
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, s._a, sizeof(int) * s._top);
_top = s._top;
_capacity = s._capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
void Print()
{
int i = 0;
for (i = 0; i < _top; i++)
{
cout << _a[i] << " ";
}
cout << endl;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Print();
Stack s2(s1);
s2.Print();
return 0;
}