类的默认成员函数
构造函数
主要完成初始化工作,实例化对象时编译器自动调用,并且只调用一次,无返回值,也可以重载
编译器生成默认的构造函数有一个作用就是:会调用自定类型成员的默认成员函数
需要注意的是:一个类中 只能有一个默认构造函数 无参数的构造函数和全缺省的构造函数都称为默认构造函数;
例如
class Date{
public:
Date(){
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
system("pause");
return 0;
}
这个类里面就有两个默认构造函数,在 main 函数里在定义类对象的时候由于没有初始化,所以会调用默认的构造函数,但是编译器不知道该调用哪个,这时候就会出错,随便去掉注释掉一个构造函数就可以了,也可以把 main 函数里的 Date 给予初始化 ,比如Date(2019,5,17), 这时候虽然有两个默认构造函数,但是编译器不会出错,因为它会忽略掉无参数的构造函数,调用缺省参数的构造函数
一般情况下,成员变量都是在构造函数的初始化列表中初始化,其中引用成员变量、const 成员变量、类类型成员必须放在初始化列表中初始化,
析构函数
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数是特殊的成员函数。
- 析构函数名和类名相同,但是要加一个字符 ~
- 无参数 无返回值
- 一个类有且只有一个析构函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
编译器生成的默认析构函数 和 默认构造函数有一个共同的特点,对会自定类型成员调用它的析构函数,
class String{
public:
String(const char* str = "jack"){
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String(){
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person{
private:
String _name;
int _age;
};
int main(){
Person p;
return 0;
}
这时候会打印出 “~String()”
拷贝构造函数
- 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参 使用传值方式会引发无穷递归调用
例如
#include<iostream>
using namespace std;
class CExample{
private:
int m_nTest;
public:
CExample(int x) : m_nTest(x) //带参数构造函数
{
cout << "constructor with argument"<<endl;
}
// 拷贝构造函数,参数中的const不是严格必须的,但引用符号是必须的
CExample(const CExample & ex) //拷贝构造函数
{
m_nTest = ex.m_nTest;
cout << "copy constructor"<<endl;
}
CExample& operator = (const CExample &ex) //赋值函数(赋值运算符重载)
{
cout << "assignment operator"<<endl;
m_nTest = ex.m_nTest;
return *this;
}
void myTestFunc(CExample ex)
{
}
};
int main(void)
{
1 CExample aaa(2);
2 CExample bbb(3);
3 bbb = aaa;
4 CExample ccc = aaa;
5 bbb.myTestFunc(aaa);
6
7 return 0;
}
- 第一行输出的是 constructor with argument 意味着调用的是构造函数,为什么会调用构造函数,是因为第一条语句它是一个实例化,在实例化的时候编译器调用构造函数,这个比较简单~
- 同上~~~
- 这个要稍微注意一下了:它调用的赋值函数,是因为aaa bbb 已经实例化了,所以没有对象需要被构造,自然就不会调用拷贝构造函数
- 第四行输出 copy constructor 调用的是拷贝构造函数,该语句也可以这样写CExample ccc (aaa);两者等价;那 ,,为什么这条语句会调用拷贝构造函数呢?是因为 ccc 是一个未被实例化的对象,所以需要被构造。
- 第五行语句可以这样想,bbb调用了函数,因为是aaa作为参数传进去,等价 CExample ex = aaa;然而ex 也没有被实例化,所以编译器会自动构造ex 调用拷贝构造函数;
我们学习C语言的时候就知道,如果传引用直接是对实参进行操作;如果函数参数是传值的方式,,那么形参是实参的一份拷贝,也就是说,这份拷贝并没有被实例化,那么就会再次调用拷贝构造函数,那么又会为参数生成一份副本,这个副本依然是未被实例化的,又会调用拷贝构造函数,就会一直递归下去。。。
稍微举个列子,就拿刚才的代码来说:我们假设是按传值的方式
CExample(const CExample ex) { }//这是拷贝构造函数
CExample ddd = aaa; //aaa作为参数调用拷贝构造函数,就等价于 CExample ex = aaa;很明显ex没有被实例化,就又会调用该拷贝构造函数,每一次的调用,它的形参都是没有被实例化的;那么指针行吗? 指针也不行,因为指针传递的是地址,其实也相当于传值;
浅拷贝
若未显示定义,系统生成默认的拷贝构造函数。 默认的 拷贝构造函数对象 按 内存存储按字节序 完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
这种浅拷贝有一定的缺陷,如果涉及到内存的申请,进行值拷贝后就会把两个对象指向同一块内存区域,这样就会导致在析构函数中多次释放内存空间而造成泄露,那要怎么解决呢?后面我总结深拷贝的时候 再把两个拿出来做一个详细对比,这里先跳过~~!!!
赋值操作符重载
赋值运算符主要有四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。这里的按值拷贝其实很危险,和之前一样,如果申请了内存空间,那么按值拷贝就会把两个对象(变量)指向同一块内存空间,导致再释放空间的时候造成内存泄露!!不过下面这个代码程序是安全的~~
class A{
public:
//构造函数
A(int a = 10, int b = 20){
_a = a;
_b = b;
}
//赋值操作符重载
A& operator=(const A& c){
if (this != &c){
//这里同样也是 省略了this指针
_a = c._a;
_b = c._b;
}
return *this;
}
private:
int _a;
int _b;
};
int main(){
A a1(100, 200);
A a2;
a2 = a1;
system("pause");
return 0;
}
从监视器中我们可以看到起初值不一样
当执行 a2 = a1;语句后变为一样的值
取地址操作符重载
取地址运算符需要注意的是:
- 没有显示参数,但有一个this 指针,指向当前对象
- 函数返回值是一个指针,所以最后返回 this
class B{
public:
//构造函数
B(int a = 10 ,int b = 20){
_a = a;
_b = b;
}
//取地址运算符重载
B* operator&(){
return this;
}
private:
int _a;
int _b;
};
int main(){
B b1(100, 200);
&b1;
system("pause");
return 0;
}
从监视器可以看到 构造 b1 后的值,以及 this 和 &b1 的地址是一样的
const 成员
const 修饰类的成员函数:将 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改
cons 修饰的取地址操作符重载 和 取地址操作符重载一般不需要自己实现,因为编译器会自主帮我们实现,除非要获取指定的内容,我们才自己去实现重载。
B* operator&()const{
return this;
}
const B* operator&()const{
return this;
}
接下来思考4个很绕的问题
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
首先来看第一个: const对象调用非const成员函数
class Date{
public :
void Display (){
cout<<"Display ()" <<endl;
cout<<"year:" <<_year<< endl;
}
private :
int _year ; // 年
};
int main(){
const Date d2;
d2.Display ();
return 0;
}
编译过不了,说的是类型不兼容;可以这样理解:是因为成员函数没有加const ,用户可能会在里面修改成员变量值,这样就会很不安全,所以编译器为了避免这种风险,在编译的时候就报错!!
接下来再看第二个:非const对象调用const成员函数
class A{
public:
void display()const{
}
private:
int a;
};
int main(){
A a1;
a1.display();
system("pause");
return 0;
}
顺利通过编译,因为这样操作是安全的,定义一个 a1 对象,它是可以修改的,就相当于是可读可写,但是成员函数是仅仅可读(不可以被修改)可以理解为:“大权限兼容小权限” 如果在成员函数里面对 对象进行修改,编译器一样也会报错!!
const 成员函数内调用其它的非 const 成员函数,非 const 成员函数内调用其它的 const 成员函数?也可以把这个和之前的权限做一个形象对比,由于 const 成员函数是可读的,所以里面不可以调用一个可读可写的函数,这样就可能会出现数据被修改的安全问题,但是非 const 成员函数是可读可写的,里面可以包含一个const 函数(仅仅可读)因为大权限兼容小权限。
这里只是把权限做一个比喻而已,仅仅是为了理解方便~~!!!
C++为了增强 代码的可读性 引入了运算符重载
.* :: sizeof ? : . 这5个运算符不能重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数原型:返回值类型 operator操作符(参数列表)
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型或者枚举类型的操作数用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1);
Date(const Date& d);
Date& operator=(const Date& d);
Date operator+(int days);
Date operator-(int days);
int operator-(const Date& d);
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
bool operator>(const Date& d)const;
bool operator>=(const Date& d)const;
bool operator<(const Date& d)const;
bool operator<=(const Date& d)const;
bool operator==(const Date& d)const;
bool operator!=(const Date& d)const;
private:
int _year;
int _month;
int _day;
};