文章目录
- 前言
- 一、构造函数
- 二、析构函数
- 三、拷贝构造函数
- 四、重载赋值操作符
- 五、取地址及const取地址操作符重载
前言
默认成员函数是编译器自动生成的,也可以自己重写,自己重写之后编译器就不再生成,下面是深入了解这些成员函数。
一、构造函数
1、构造函数的特征:
(1). 函数名与类名相同。
(2). 无返回值。
(3). 对象实例化时编译器自动调用对应的构造函数。
(4). 构造函数可以重载。
如:
//构造函数
class test01
{
public:
//构造函数 - 无参构造函数 无返回值,与类名相同
test01()
{
cout << "test01()" << endl;
}
//构造函数的重载 - 有参构造 可以重载
test01(int a)
{
cout << "test01(int a)" << endl;
}
private:
int _a;
};
int main()
{
test01 p1; //会自动调用无参构造
test01 p2(10); //自动调用有参构造
return 0;
}
2、需要注意的点
(1)调用无参构造是不要加()
,不然就成函数声明了
如:
//如函数 void Add() void - 类型 Add - 函数名 () - 参数列表
//test01 p1(); test01 -类型 p1 - 函数名 () - 参数列表 错误使用
test01 p1; //正确使用
(2)无参构造与全缺省重载的构造函数
如:
// 构造函数
class test02
{
public:
//构造函数 - 无参构造函数 无返回值,与类名相同
test02()
{
cout << "test02()" << endl;
}
//构造函数的重载 - 有参构造 可以重载
test02(int a = 10)
{
cout << "test02(int a)" << endl;
}
private:
int _a;
};
int main()
{
test02 p1;
return 0;
}
会出现什么情况捏?
答:出现对重载函数调用的不明确
(3)当我们使用编译器给我生成的默认构造函数时,那么类内的成员变量是否会被初始化呢?
对于内置类型来说:没有初始化出现随机值
对于自定义类型来说:有不需要参数的构造函数就会调用,没有就不会调用。
如:
//结构体
typedef struct N
{
//构造
N() { cout << "struct N" << endl; }
int i;
}N;
//联合体
union E
{
E() { cout << "union E" << endl; }
int i;
};
//类
class test02
{
public:
test02()
{
cout << "test02()" << endl;
}
private:
int _a;
};
// 构造函数
class test03
{
public:
private:
//内置类型
int _a;
//自定义类型
test02 p;
N n;
E e;
};
int main()
{
test03 p1;
return 0;
}
3、给成员变量默认值
给成员变量默认值后如果不对其进行赋初值的话,就会使用该默认值。
//构造函数的默认值
class test02
{
public:
test02() //不进行任何赋值
{
//...
};
test02(int a,int b)
{
//...
_a = a;
_b = b;
}
void Print()
{
cout << _a << endl << _b << endl;
}
private:
//给变量初始默认值
int _a = 10;
int _b = 10;
};
int main()
{
test02 a; //使用无参构造
test02 b(20, 20); //有参构造
a.Print();
b.Print();
return 0;
}
4、初始化列表
(1)
//这算初始化吗?
test02(int a,int b)
{
_a = a;
_b = b;
}
答:这不是初始化,因为初始化只能初始化一次,但是在构造函数里可以进行多次的赋值,这只能算是赋初值。
(2)初始化列表格式
在构造函数后面加双引号,在双引号后面加成员变量和括号,括号里是要给成员变量初始化的值。
test03(int a, int b) :_a(a), _b(b)
{
//,,,
}
(3)注意
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量 const成员变量 自定义类型成员(且该类没有默认构造函数时)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
//初始化列表
class A
{
public:
};
class test03
{
public:
//初始化顺序按声明顺序初始化
test03(A a, int b,int c) :_c(c),_a(a),_b(b)
{
//,,,
}
private:
//自定义类型
A _a;
//const成员
const int _b;
//引用
int& _c;
};
5、作用:
完成初始化工作。
如:
初始化栈
//初始化栈
class Stack
{
public:
Stack(int capacity = 4) //利用全缺省参数
{
_capacity = capacity;
_top = 0;
//申请空间
_a = new int[_capacity];
}
private:
//数组指针
int* _a;
//容量
int _capacity;
//栈顶后一个位置
int _top;
};
二、析构函数
1、析构函数的特征
(1) 函数名 :
~
加上类名。
(2)不能重载。
(3)没有返回值和参数。
(4)在程序结束时自动调用。
如:
class test04
{
public:
//析构函数 无返回值无参数 不能重载 结束自动调用
~test04()
{
cout << "~test04()" << endl;
}
};
int main()
{
test04 p;
return 0;
}
2、需要注意的点
(1)使用编译器自动生成的析构函数在结束时,对于内置类型来说不做处理,因为结束后系统会自动回收,对于自定义类型来说,默认的析构函数会调用成员变量的析构函数进行对该成员变量的清理。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
class test04
{
public:
~test04()
{
cout << "~test04()" << endl;
}
};
class test05
{
public:
private:
//内置类型
int _a;
//自定义类型
test04 p;
};
int main()
{
test05 p;
return 0;
}
(2)当我们使用编译器生成的析构函数时,该函数对会怎么处理捏?
对内置类型不做处理,对自定义类型会调用其析构函数。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
//自定义类型
A p;
//内置类型
int a;
};
int main()
{
B p;
return 0;
}
那么申请的资源会被清理吗?
答:是不会的,所以存在需要清理申请的资源时,析构一定要重写。
3、作用
清理申请的资源。
如果没有申请的资源的话不重写用编译器生成的析构函数也行,当我们申请了资源就必须重写析构函数来释放申请的资源了。
如栈的释放:
class Stack
{
public:
//构造函数
Stack(int capacity = 4) //利用全缺省参数
{
_capacity = capacity;
_top = 0;
//申请空间 有申请的空间需要在析构函数里释放
_a = new int[_capacity];
}
//析构函数
~Stack()
{
//释放申请的空间
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
//数组指针
int* _a;
//容量
int _capacity;
//栈顶后一个位置
int _top;
};
三、拷贝构造函数
1、拷贝构造的特征
(1)拷贝构造是构造函数的一种重载。
(2)只有一个成员,就是该类的一个引用。函数名如: test06(test06 & a)(test06是一个类)
如:
class test06
{
public:
test06(int a,int b):_a(a),_b(b)
{}
//拷贝构造
test06(test06& a)
{
_a = a._a;
_b = a._b;
}
void Print()
{
cout << _a << " " << _b<<endl;
}
private:
int _a ;
int _b ;
};
int main()
{
test06 a(20,20);
//test06 b(a); 与下面等价
test06 b = a;
a.Print();
b.Print();
return 0;
}
2、需要注意的点
(1)如果传的参数不是引用会发生什么?
答:会出现无尽递归。
(2)当使用编译器生成的默认拷贝构造函数时会怎么样?
对内置类型:进行值拷贝。
对自定义类型:调用其拷贝构造函数。
class A
{
public:
A(int a) :_a(a)
{
}
//拷贝构造
A(A& a)
{
cout << "A(A& a)" << endl;
}
int _a;
};
class test06
{
public:
test06(A a, int b) :_a(a), _b(b)
{}
private:
//自定义类型
A _a;
//内置类型
int _b;
};
int main()
{
A a(10);
test06 b(a, 20);
test06 c(b);
return 0;
}
(3)浅拷贝和深拷贝
浅拷贝也叫值拷贝就是按照字节序的方式直接进行拷贝
如:
test06(test06 &a)
{
_a = a._a;
_b = a._b;
}
下面那样还能用浅拷贝完成吗?
class test07
{
public:
test07(int* a):_a(a)
{
}
~test07()
{
delete[]_a;
}
test07(test07& p)
{
_a = p._a;
}
private:
int* _a;
};
int main()
{
int* a = new int[10];
for (int i = 0; i < 10; i++)
{
a[i] = i;
}
test07 p(a);
test07 pp(p);
return 0;
}
答案是:不能的,因为当p._a
和pp._a
指的是同一块空间,当p
析构之后,p._a
指向的空间就被释放了,当pp
再使用pp._a
时就会出现问题,以及pp
析构时又会对这块空间进行析构,这样就会造成重复释放导致错误。
为了解决这个问题,我们使用深拷贝
如:
test07(test07& p)
{
//重新开辟空间
int* _a = new int[10];
//拷贝
memcpy(_a, p._a, sizeof(int)*10);
}
总结:
当遇到需要申请空间之类的成员变量时,需要重写拷贝构造函数并使用深拷贝,不然使用系统默认生成的也可以。
3、作用
初始化对象。
四、重载赋值操作符
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
返回类型 :
const 类名 &
返回类的话就可以实现连续赋值了。
函数名:operator=
。
参数类型:(const & 类名)
,因为编译器自动会传一个隐含的this
,所以我们传一个参数就够了。
实现:
class Kind
{
public:
//构造函数
Kind(int a = 10,int b = 10)
{
_a = a;
_b = b;
}
//重载 = //只能作为成员函数 const 防止被修改
const Kind & operator=(const Kind& p)
{
this->_a = p._a;
this->_b = p._b;
//返回 *this 使其可以连续赋值
return *this;
}
private:
int _a;
int _b;
};
int main()
{
Kind p1(20, 20);
Kind p2;
Kind p3;
//连续赋值
p3 = p2 = p1;
cout << "p1: " << p1._a << " " << p1._b << endl;
cout << "p2: " << p2._a << " " << p2._b << endl;
cout << "p3: " << p3._a << " " << p3._b << endl;
return 0;
}
其实默认的赋值运算符重载函数就是像上写的那样进行赋值,我们对于这样的拷贝叫做浅拷贝,这样做有一个弊端就是如果遇到动态申请的空间的话就有可能发生程序崩溃,这是因为共用一块空间当另一个对象将这块空间释放之后被赋值的那个对象再使用这块空间时就会发生崩溃。
如:
class Kind
{
public:
//构造函数
Kind(int a = 10,int b = 10)
{
_a = a;
_b = b;
arr = new int;
*arr = a;
}
//重载 = //只能作为成员函数 const 防止被修改
const Kind & operator=(const Kind& p)
{
this->_a = p._a;
this->_b = p._b;
this->arr = p.arr;
//返回 *this 使其可以连续赋值
return *this;
}
private:
int _a;
int _b;
int* arr;
};
int main()
{
Kind p1(20, 20);
Kind p2 = p1;
return 0;
}
当上述的p1将arr释放了,p2再使用arr就会发生崩溃。
当我们遇到这种情况时我们使用深拷贝。
//深拷贝
const Kind& operator=(const Kind& p)
{
this->_a = p._a;
this->_b = p._b;
int* tmp = new int;
if (tmp == nullptr)
exit(-1);
*tmp = *p.arr;
this->arr = tmp;
//返回 *this 使其可以连续赋值
return *this;
}
赋值运算符重载和拷贝构造的区别
拷贝构造是一个已经存在的类给一个刚创建的类进行初始化。
赋值运算符重载是一个已经存在的的另一个已经存在的类赋值。
如:
A a;
//只有a已经存在 使用拷贝构造
A b = a;
A c;
A d;
//c\d都是已经存在的了, 使用赋值运算符重载
c = d;
总结:
当遇到动态申请的空间时需要重写赋值运算符,如果没有用编译器自动生成的即可。
五、取地址及const取地址操作符重载
1、const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如:
class test08
{
public:
void Print() const //等同与 const * this
{
cout << _a;
}
private:
int _a;
};
(1)const对象可以调用非const成员函数吗?
答:不可以,属于权限放大了。(2)非const对象可以调用const成员函数吗?
答:可以,属于权限缩小。
(3)const成员函数内可以调用其它的非const成员函数吗?
答:不可以,属于权限放大了。(4) 非const成员函数内可以调用其它的const成员函数吗?
答:可以,属于权限缩小。
2、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class test08
{
public :
//取地址
test08* operator&()
{
return this ;
}
// const取地址
const test08* operator&()const
{
return this ;
}
};