1.类的6个默认函数
在一个空类当中,是真的没有任何成员吗?并不是,空类当中会生成6个默认函数。 默认成员函数不会显示地展示给我们看,而是编译器自动生成的隐式的成员函数。
本片文章将会介绍4个内容,分别为:构造函数、析构函数、拷贝构造函数以及运算符重载。这四个内容将是学习C++碰到的第一块难啃的骨头。
2.构造函数
2.1构造函数的概念与基本使用
构造函数没有构造什么东西,它只是帮我们完成初始化的工作。同样是初始化,我们为什么不能自己写一个初始化呢?正是因为这样,所以才要有构造函数,构造函数是编译器自动调用的函数,不需要我们去手动调用。也就是说,在我们定义对象的时候,就完成了初始化的工作。
class A
{
public:
void Init()
{
_a = 0;
_b = 0;
}
private:
int _a;
int _b;
};
int main()
{
A a1;
a1.Init();
return 0;
}
class A
{
public:
A()
{
_a = 0;
_b = 0;
}
private:
int _a;
int _b;
};
int main()
{
A a1;
return 0;
}
构造函数的函数名与类名同名,并且没有返回值(返回类型不是void,而是函数名前没有任何返回值)。在我们定义对象的时候,编译器自动调用构造函数完成初始化工作。
并且构造函数支持重载:
class A
{
public:
A()
{
_a = 0;
_b = 0;
}
A(int a, int b)
{
_a = a;
_b = b;
}
void Show()
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
A a1(1, 2);
a1.Show();
A a2;
a2.Show();
return 0;
总结: 1.构造函数编译器自动调用的完成初始化工作的函数 2.构造函数的函数名与类名相同,且不具有返回值 3构造函数支持重载
2.2编译器自动生成的默认构造函数
当我们不手动定义构造函数的时候,编译器会自动生成一个无参的默认构造函数。如果我们手动定义构造函数,那么编译器将不会自动生成默认构造函数。编译器生成的构造函数会起到什么效果呢?
class A
{
public:
void Show()
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
A a1;
a1.Show();
return 0;
}
可以看到自动生成的默认构造函数没有起任何作用?事实上真是这样吗?
class B
{
public:
B()
{
_x = 1;
_y = 1;
cout << _x << " " << _y << endl;
}
private:
int _x;
int _y;
};
class A
{
public:
void Show()
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
B b1;
};
int main()
{
A a1;
a1.Show();
return 0;
}
可以看到,我们似乎调用了B类的构造函数,然后再调用Show函数。这说明了编译器自动生成的默认构造函数不对内置类型处理,而是调用自定义类型的构造函数。
在以前版本的C++语言中,如果我们不手动定义构造函数的话,那么编译器生成的默认构造函数看起来就似乎就没有意义了。而C++标准委员会也认识到了这一点,所以在此基础上打了一个补丁,即:内置类型的成员变量可以给定缺省值。
class SeqList
{
public:
private:
int* a = (int*)malloc(sizeof(int));
int size = 20;
};
int main()
{
SeqList sl;
return 0;
}
总结: 1.当我们没有手动定义构造函数时,编译器会自动生成一个默认构造函数 2.编译器生成的默认构造函数不对内置类型进行处理,编译器会调用自定义类型的构造函数 3.C++11中,可以为类中的成员变量添加缺省值,目的是为了弥补编译器生成的默认构造函数的不足
2.3默认构造函数
并不是编译器自动生成的默认构造函数才称为默认构造函数。无参的、全缺省的构造函数都可以称为默认构造函数。与缺省参数和函数重载一样,我们不要定义具有矛盾的构造函数。
class Date
{
public:
Date()
{
_year = 2000;
_month = 1;
_day = 1;
}
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
3.析构函数
3.1析构函数的概念与基本使用
与构造函数相反,析构函数的作用是对空间的释放。它也是编译器自动调用的。不过我们需要注意,我们并不是销毁对象本身,而是去释放向操作系统申请的空间。例如我们使用malloc函数申请得到一块空间,那么在程序结束之前,需要使用free函数释放掉这一块空间,否则有可能会造成内存泄漏。
class Stack
{
public:
~Stack()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
cout << "析构函数" << endl;
}
private:
int* _a=(int*)malloc(sizeof(int)*_capacity);
int _top=0;
int _capacity=4;
};
int main()
{
Stack s1;
return 0;
}
由此可见,析构函数的函数名与类名相同,不过需要在函数名之前加上~,并且没有返回值;析构函数在对象的所处的函数栈帧销毁时自动调用;析构函数不支持重载,每一个类有且只有一个析构函数。
3.2编译器自动生成的默认析构函数
与构造函数一样,当我们没有显示定义析构函数时,编译器会自动生成一个默认析构函数。同样的,默认析构函数不会对内置类型进行处理,而是调用自定义类型中的析构函数。
class Stack
{
public:
~Stack()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
cout << "析构函数" << endl;
}
private:
int* _a=(int*)malloc(sizeof(int)*_capacity);
int _top=0;
int _capacity=4;
};
class A
{
public:
private:
int _a=0;
int _b=0;
Stack s1;
};
int main()
{
Stack A;
return 0;
}
3.3析构函数的使用环境
当类中没有向系统申请空间时,我们不需要定义任何析构函数,因为函数栈帧销毁时,会连同在栈帧中的局部变量一起销毁。就比如日期类。
当类中存在向系统申请的空间时,我们必须定义析构函数。因为栈帧的销毁不影响堆中的空间。例如栈类、队列类。
4.拷贝构造函数
4.1拷贝构造函数的概念
假如我们有两个整型变量,我们要使这两个变量的值相等,我们赋值运算符即可。
int a = 3;
int b = a;
那么对象与对象之间能否进行赋值呢?可以,不过我们需要拷贝构造函数,我们可以自定义对象与对象之间的哪些成员赋值。
拷贝构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2拷贝构造函数的基本使用
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,9,24);
Date d2(d1);
return 0;
}
可以看到,拷贝构造函数构造函数的重载,它的参数是对象的引用。为什么一定要对象的引用?一方面是节省形参和指针所开辟的空间,第二个原因在于,如果形参不使用引用或指针,那么形参将会是实参的一份临时拷贝。那么形参的值将从实参拷贝过来,既然是拷贝,那么就会调用拷贝构造函数,从而造成死递归。
我们可以观察一下d2对象的成员是否与d1对象的成员一致:
4.3浅拷贝
当我们只需要对值进行拷贝的时候,我们就可以显示定义拷贝构造函数的浅拷贝。当我们没有显示定义拷贝构造函数时,编译器会自动生成一个默认拷贝构造函数,这个默认拷贝构造函数是进行浅拷贝的函数。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,9,24);
Date d2(d1);
return 0;
}
如果我们的类是一个栈类,那么浅拷贝不足以帮助我们完成任务:
class Stack
{
public:
Stack(size_t capacity = 10)
{
_a = (int*)malloc(capacity * sizeof(int));
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
_a[_top++] = x;
}
Stack(Stack& s)
{
_a = s._a;
_top = _top;
_capacity = s._capacity;
}
private:
int* _a ;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
可以看到,如果使用浅拷贝,s2中的_a并没有指向s2独立开辟的空间,而是指向s1中开辟的空间。即浅拷贝只拷贝了地址。那么这就会造成s1中的对象改变,s2指向的对象也会改变。这并不是我们想要看到的结果。 并且,如果我们定义了析构函数,栈是后进先出。所以函数结束时,会先释放s2,然后再释放s1。又因为s2和s1的_a指向的是同一块空间,而s2已经对这块空间释放过了,那么s1再释放,就会发生错误。
4.4深拷贝
同样以栈类作为案例:
class Stack
{
public:
Stack(size_t capacity = 10)
{
_a = (int*)malloc(capacity * sizeof(int));
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
_a[_top++] = x;
}
Stack(const Stack& s)
{
_a = (int*)malloc(sizeof(int) * s._top);
assert(_a);
memcpy(_a, s._a, sizeof(int) * s._capacity);
_top = s._top;
_capacity = s._capacity;
}
private:
int* _a ;
int _top;
int _capacity;
};
我们使用深拷贝便能将每个对象的空间独立开来。
总结: 1.当我们的类没有涉及申请空间的时候,可以不写拷贝构造函数或者定义浅拷贝的拷贝构造函数 2.如果类的设计涉及到了申请空间,那么拷贝构造函数一定要写,并且执行的操作是深拷贝
|