简单记下关于C++类一些笔记。
构造、析构、 拷贝构造、赋值
结构体(struct)
说类(class)之前,说下结构体。在C++中,结构体是一种特殊形态的类,区别如下:
结构体和类的唯一区别就是,结构体和类具有不同的默认访问控制属性。
- class: 对于未指定访问控制属性的成员,其默认属性为private;
- struct: 对于未指定任何访问控制属性的成员,其默认属性为public;
在C++中,结构体同样具有构造函数、析构函数、拷贝构造函数、赋值函数等函数。而且,类(class)还可以与结构体(struct)相互继承。
下面以一个结构体为例,来简单记下构造等函数的基本写法:
struct DataPack
{
int size;
char* name;
};
构造函数与析构函数
如果没有定义构造函数与析构函数,C++则会定义默认的构造函数析构函数,默认的形式如下:
struct DataPack
{
int size;
char* name;
DataPack(){}
~DataPack(){}
};
- 构造函数用于在创建对象时,给对象的成员进行赋值,不需要赋值用默认的即可;
- 析构函数用于在消毁对象时,释放对象中指针成员指向的内存;
拷贝构造函数与赋值函数
同样,C++会定义默认的拷贝构造函数和赋值函数,如果没有的话。正因为有默认的,以下一般性的代码才可以执行:
DataPack pack;
DataPack newpack1 = pack; // 对象初始化,调用拷贝构造函数
DataPack newpack2(pack); // 对象初始化,调用拷贝构造函数
DataPack newpack3;
newpack3 = pack; // 对象赋值,调用赋值函数(operator)
但默认的拷贝构造函数和赋值函数是浅拷贝,即将两个对象对应的成员进行简单的赋值,若是有指针成员,在对象析构时会发生错误,拿指针来说,两个对象经的指针指向同一块内存,析构时,这块内存就会被delete两次,这明显不行。所有就需要深拷贝了,如下所示:
struct DataPack
{
int size;
char* name;
// 浅拷贝构造函数
DataPack(const DataPack& dp){
this->size = dp.size;
this->name = dp.name;
}
// 深拷贝构造函数
DataPack(const DataPack& dp){
this->size = dp.size;
this->name = new char[this->size];
memcpy(this->name, dp.name, this->size);
}
// 浅拷贝赋值函数
DataPack& operator=(const DataPack& dp){
this->size = dp.size;
this->name = dp.name;
return *this;
}
// 深拷贝赋值函数
DataPack& operator=(const DataPack& dp){
this->size = dp.size;
delete this->name; // 先释放原来的内存
this->name = new char[this->size];
memcpy(this->name, dp.name, this->size);
return *this;
}
};
拷贝构造函数的几点说明
- 默认拷贝构造函数没有处理static数据成员;
- 拷贝构造函数必须是引用传递,如果是值传递,则会无限的调用拷贝构造函数,因为值传递本身就是先要调用拷贝构造函数;如果是指针传递,则只是构造函数,而不是拷贝构造函数;
- 拷贝构造函数中不受private限制,即可以直接访问private成员变量;
- 对于一个类X, 如果一个构造函数的第一个参数是下列之一,且没有其他参数,或其他参数都有默认值,那么这个函数是拷贝构造函数.
(1) X& (2) const X& (3) volatile X& (4) const volatile X&
赋值函数(operator=)的几点说明
- 赋值函数的前提是对象已经初始化,所在赋值函数中,对象先会丢弃原有的值(指针则先要释放内存),再赋予新的值(指针则重新申请内存);
public, protected, private与继承
访问权限区别
访问属性 | 说明 |
---|---|
public | 可以被对象实体直接访问 |
protected | 只允许在本类和子类中访问,对象实体通过public员函数访问 |
private | 只允许在本类中访问,对象实体通过public员函数访问 |
继承方式与权限的关系
基类访问属性 | 继承方式 | 子类访问属性 |
---|---|---|
public | public | public |
public | protected | protected |
public | private | private |
protected | public | protected |
protected | protected | protected |
protected | private | private |
private | public,protectd,private | 子类无权访问 |
基类指针指向派生类实例的原理
简单的用图来解释下。基类指针可访问的内存地址长度,比派生类指针可访问的内存地址长度要短。C++中可以用基类指针指向派生类实例,但base_ptr可访问的地址长度只限于BaseClass范围,如下图所示;反之,用派生类指针指向基类实例则是不允许的,因为derived_ptr访问BaseClass范围之外的地址时,则会发生指针越界
base_ptr |---------------|---------------| derived_ptr
| | | | |
| |Base class | Derived Class | |
| | | | |
- |---------------| | |
| | |
| | |
| | |
|---------------| -
C++异常处理中的栈回退机制(stack unwind)
异常处理涉及了函数之间的跳转(goto只是函数局部调转),比如c中用的setjump、longjump。
C++中的异常处理,是在函数调用堆栈中添加了栈回退机制。
- 栈回退需要释放throw内声明的临时变量等资源;
- 栈回退需要在当前函数中查找try对应的catch代码块,若未找到,则回退到上一级函数继续查找;
try {
func(); // 栈回退实现了try中catch到func内部的throw
} catch(){...}
void func() {
ClassA a;
ClassB b;
throw(); // throw后,需要释放a和b,故出现异常时,通过栈回退来调用a和b的析构函数;
// 若在异常,析构函数中再次引发异常,则会强行停止进程;
// 故在析构函数中不应该throw,即使throw也最好在析构函数内部就catch并处理;
ClassC c;
}
#include <iostream>
class A {
~A() {
// throw 1; // 会在异常中再次引发引常
try {
throw 1;
} catch (int t) {
std::cout << t << std::endl;
}
}
};
void func() {
A a();
throw 2;
}
int main(void)
{
try {
func();
} catch (int t) {
std::cout << t << std::endl;
}
return 0;
}
异常处理涉及了函数之间的跳转(goto只是函数局部调转),比如c中用的setjump、longjump。
C++中的异常处理,是在函数调用堆栈中添加了栈回退机制。
- 栈回退需要释放throw内声明的临时变量等资源;
- 栈回退需要在当前函数中查找try对应的catch代码块,若未找到,则回退到上一级函数继续查找;
try {
func(); // 栈回退实现了try中catch到func内部的throw
} catch(){...}
void func() {
ClassA a;
ClassB b;
throw(); // throw后,需要释放a和b,故出现异常时,通过栈回退来调用a和b的析构函数;
// 若在异常,析构函数中再次引发异常,则会强行停止进程;
// 故在析构函数中不应该throw,即使throw也最好在析构函数内部就catch并处理;
ClassC c;
}
#include <iostream>
class A {
~A() {
// throw 1; // 会在异常中再次引发引常
try {
throw 1;
} catch (int t) {
std::cout << t << std::endl;
}
}
};
void func() {
A a();
throw 2;
}
int main(void)
{
try {
func();
} catch (int t) {
std::cout << t << std::endl;
}
return 0;
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]