类的四个默认函数
父子类构造函数问题父子类析构函数问题
拷贝构造函数的深浅copy
赋值函数的深浅copy
C++类有四个默认函数
分别是:(Effective C++》中提到四个)
1、默认构造函数;
2、默认拷贝构造函数;
3、默认析构函数;
4、赋值运算符函数;
// 这两个类的效果相同
class Empty
{}
class Empty
{
public:
Empty() {} // deafault构造函数;
Empty(const Empty&) {} // 默认拷贝构造函数
~Empty() {...} // 析构函数
Empty& operator = (const Empty &) {} // 赋值运算符
}
说明:
1) 这些函数只有在需要调用的时候,编译器才会生成。
2) 这些函数都是public的。
3) 这些函数都是inline的(即函数定义在类的定义中的函数)。
4) 如果你显式的声明了这些函数,那么编译器将不再生成默认的函数。
比如,当遇到下列语句时,函数会被编译器生成:
Empty e1; //默认构造函数
//对象销毁时,析构函数
Empty e2(e1); //拷贝构造函数
Empty e3 = e1;//拷贝构造函数
e2 = e1; //赋值运算符重载函数
取地址运算符函数?
另外两种默认的函数:就是取地址运算符和取地址运算符的const版本,这两个函数在《Effective C++》中没有提及。
Empty *operator &() {} // 取值运算符
const Empty *operator &() const {} // 取值运算符const
正如下面的代码可以正常工作:到底是通用的取地址符作用,还是这两个函数是确实存在呢?可能是默认的取地址吧。默认的取地址符就是做这个工作吧。
#include <stdio.h>
class Empty {};
int main(int argc, char** argv){
Empty a;
const Empty *b = &a; printf("%p/n", &a); //调用取地址运算符
printf("%p/n", b); //调用const取地址运算符
}
构造函数
父子类构造函数
构造方法用来初始化类的对象,与父类的其它成员不同,它不能被子类继承(子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。
构造原则如下:
1、没有定义构造函数,编译器会给一个默认的构造函数(无参数,无内容)。
2、定义了构造函数,不论是无参还是有参,编译器不再提供默认构造函数。
在C++的类继承中,建立对象时,首先调用父类的构造函数,然后在调用下一个子类的构造函数,依次类推;析构对象时,其顺序正好与构造相反,先调用子类的析构函数,在调用下一个父类的构造函数,依次类推;
1、子类未显示调用父类的构造函数,会调用父类的无参构造函数。此时如果父类只有有参数的构造方法,则会出错(如果 父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)2、 子类显示调用父类的构造函数,使用初始化列表来调用父类构造函数
父子类析构函数
在C++的类继承中,析构对象时,其构造对象时顺序正好与构造相反,先调用子类的析构函数,在调用下一个父类的构造函数,依次类推;
如果是父类指针,指向子类对象,那么析构的时候有可能会出现不调用子类析构函数的情况(如果父类析构函数不是virtual的话),这种情况下可能会出现内存泄漏问题(如果子类在析构函数里面进行了内存资源释放)。
所以,如果一个类会被继承,那么这个类的析构函数就加上virtual,设为虚析构函数。
就是说 父类的析构函数,一定要加virtual,写成虚析构函数。这样可以避免可能出现的内存泄漏。
拷贝构造函数
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
一个容易被忽略的问题: 自定义的拷贝构造函数不仅会覆盖默认的拷贝构造函数,也会覆盖默认的构造函数。下面的代码是编译不过的,用户必须再显式的定义一个无参的构造函数。
class Empty {
public: Empty(const Empty& e) { }
//拷贝构造函数};
int main(int argc, char** argv){ Empty a;}
以下情况都会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化
赋值运算符函数
一般地,赋值运算符函数的参数是函数所在类的const类型的引用加const是因为:
1、我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
2、加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
用引用是因为:
这样可以避免在函数调用时对实参的一次拷贝,提高了效率。
一般地,返回值是被赋值者的引用,即*this(如上面例1),原因是
1、这样在函数返回时避免一次拷贝,提高了效率。
2、更重要的,这样可以 实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。
赋值运算符函数限制
- 赋值运算符函数只能是类的非静态的成员函数
- 赋值运算符函数不能被继承
- 赋值运算符函数要避免自赋值: 对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。
浅拷贝和深拷贝
拷贝构造函数和赋值函数都有深浅拷贝的问题
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候, 资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝(默认拷贝构造函数和赋值函数都是浅拷贝)。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
下面举个深拷贝的例子。
#include <iostream>
using namespace std;
class CA
{
public:
CA(int b,char* cstr)
{
a=b;
str=new char[b];
strcpy(str,cstr);
}
CA(const CA& C)
{
a=C.a;
str=new char[a]; //深拷贝
if(str!=0)
strcpy(str,C.str);
}
~CA()
{
delete str;
}
private:
int a;
char *str;
};
拷贝构造函数和赋值函数的深拷贝例子,来源:https://www.cnblogs.com/zpcdbky/p/5027481.html
class MyStr
{
private:
char *name;
int id;
public:
MyStr() {}
MyStr(int _id, char *_name) //constructor
{
cout << "constructor" << endl;
id = _id;
name = new char[strlen(_name) + 1];
strcpy_s(name, strlen(_name) + 1, _name);
}
MyStr(const MyStr& str)
{
cout << "copy constructor" << endl;
id = str.id;
if (name != NULL)
delete name;
name = new char[strlen(str.name) + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
MyStr& operator =(const MyStr& str)//赋值运算符
{
cout << "operator =" << endl;
if (this != &str)
{
if (name != NULL)
delete name;
this->id = str.id;
int len = strlen(str.name);
name = new char[len + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
return *this;
}
~MyStr()
{
delete name;
}
};
& 引用 取地址
c++中引用数据类型和取地址符的区别是左值和右值的区别,引用是左值,取地址运算符是右值,所谓左值指的是内存中映射的存储单元,右值是存储单元中所存的数据。int &a = b;和int *p = &a; 可以解释这两种不同的关系。